<!DOCTYPE html>
<!--
Copyright 2016 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/tracing/base/base.html">

<script>
'use strict';

/**
 * @fileoverview Provides classes for representing and classifying VM regions.
 *
 * See https://goo.gl/5SSPv0 for more details.
 */
tr.exportTo('tr.model', function() {
  /**
   * A single virtual memory region (also called a memory map).
   *
   * @constructor
   */
  function VMRegion(startAddress, sizeInBytes, protectionFlags,
      mappedFile, byteStats) {
    this.startAddress = startAddress;
    this.sizeInBytes = sizeInBytes;
    this.protectionFlags = protectionFlags;
    this.mappedFile = mappedFile || '';
    this.byteStats = byteStats || {};
  }

  VMRegion.PROTECTION_FLAG_READ = 4;
  VMRegion.PROTECTION_FLAG_WRITE = 2;
  VMRegion.PROTECTION_FLAG_EXECUTE = 1;
  VMRegion.PROTECTION_FLAG_MAYSHARE = 128;

  VMRegion.prototype = {
    get uniqueIdWithinProcess() {
      // This value is assumed to be unique within a process.
      return this.mappedFile + '#' + this.startAddress;
    },

    get protectionFlagsToString() {
      if (this.protectionFlags === undefined) return undefined;
      return (
        (this.protectionFlags & VMRegion.PROTECTION_FLAG_READ ? 'r' : '-') +
          (this.protectionFlags & VMRegion.PROTECTION_FLAG_WRITE ? 'w' : '-') +
          (this.protectionFlags & VMRegion.PROTECTION_FLAG_EXECUTE ?
            'x' : '-') +
          (this.protectionFlags & VMRegion.PROTECTION_FLAG_MAYSHARE ? 's' : 'p')
      );
    }
  };

  VMRegion.fromDict = function(dict) {
    return new VMRegion(
        dict.startAddress,
        dict.sizeInBytes,
        dict.protectionFlags,
        dict.mappedFile,
        dict.byteStats);
  };

  /**
   * Node in a VM region classification tree.
   *
   * Note: Most users of this class should use the
   * VMRegionClassificationNode.fromRegions static method instead of this
   * constructor because it leads to better performance due to fewer memory
   * allocations.
   *
   * @constructor
   */
  function VMRegionClassificationNode(opt_rule) {
    this.rule_ = opt_rule || VMRegionClassificationNode.CLASSIFICATION_RULES;

    // True iff this node or any of its descendant classification nodes has at
    // least one classified VM region.
    this.hasRegions = false;

    // Total virtual size and byte stats of all regions matching this node's
    // rule (including its sub-rules).
    this.sizeInBytes = undefined;
    this.byteStats = {};

    // Array of child classification nodes if this is an intermediate node.
    this.children_ = undefined;

    // Array of VM regions. If this is an intermediate node, then the regions
    // are cached for lazy tree construction (i.e. its child classification
    // nodes yet have to be built).
    this.regions_ = [];
  }

  /**
   * Rules for classifying memory maps.
   *
   * These rules are derived from core/jni/android_os_Debug.cpp in Android.
   */
  VMRegionClassificationNode.CLASSIFICATION_RULES = {
    name: 'Total',
    children: [
      {
        name: 'Android',
        file: /^\/dev\/ashmem(?!\/libc malloc)/,
        children: [
          {
            name: 'Java runtime',
            file: /^\/dev\/ashmem\/dalvik-/,
            children: [
              {
                name: 'Spaces',
                file: /\/dalvik-(alloc|main|large object|non moving|zygote) space/,  // @suppress longLineCheck
                children: [
                  {
                    name: 'Normal',
                    file: /\/dalvik-(alloc|main)/
                  },
                  {
                    name: 'Large',
                    file: /\/dalvik-large object/
                  },
                  {
                    name: 'Zygote',
                    file: /\/dalvik-zygote/
                  },
                  {
                    name: 'Non-moving',
                    file: /\/dalvik-non moving/
                  }
                ]
              },
              {
                name: 'Linear Alloc',
                file: /\/dalvik-LinearAlloc/
              },
              {
                name: 'Indirect Reference Table',
                file: /\/dalvik-indirect.ref/
              },
              {
                name: 'Cache',
                file: /\/dalvik-jit-code-cache/
              },
              {
                name: 'Accounting'
              }
            ]
          },
          {
            name: 'Cursor',
            file: /\/CursorWindow/
          },
          {
            name: 'Ashmem'
          }
        ]
      },
      {
        name: 'Native heap',
        file: /^((\[heap\])|(\[anon:)|(\/dev\/ashmem\/libc malloc)|(\[discounted tracing overhead\])|$)/  // @suppress longLineCheck
      },
      {
        name: 'Stack',
        file: /^\[stack/
      },
      {
        name: 'Files',
        file: /\.((((jar)|(apk)|(ttf)|(odex)|(oat)|(art))$)|(dex)|(so))/,
        children: [
          {
            name: 'so',
            file: /\.so/
          },
          {
            name: 'jar',
            file: /\.jar$/
          },
          {
            name: 'apk',
            file: /\.apk$/
          },
          {
            name: 'ttf',
            file: /\.ttf$/
          },
          {
            name: 'dex',
            file: /\.((dex)|(odex$))/
          },
          {
            name: 'oat',
            file: /\.oat$/
          },
          {
            name: 'art',
            file: /\.art$/
          }
        ]
      },
      {
        name: 'Devices',
        file: /(^\/dev\/)|(anon_inode:dmabuf)/,
        children: [
          {
            name: 'GPU',
            file: /\/((nv)|(mali)|(kgsl))/
          },
          {
            name: 'DMA',
            file: /anon_inode:dmabuf/
          }
        ]
      }
    ]
  };
  VMRegionClassificationNode.OTHER_RULE = { name: 'Other' };

  VMRegionClassificationNode.fromRegions = function(regions, opt_rules) {
    const tree = new VMRegionClassificationNode(opt_rules);
    tree.regions_ = regions;
    for (let i = 0; i < regions.length; i++) {
      tree.addStatsFromRegion_(regions[i]);
    }
    return tree;
  };

  VMRegionClassificationNode.prototype = {
    get title() {
      return this.rule_.name;
    },

    get children() {
      if (this.isLeafNode) {
        return undefined;  // Leaf nodes don't have children (by definition).
      }
      if (this.children_ === undefined) {
        this.buildTree_();  // Lazily classify VM regions.
      }
      return this.children_;
    },

    get regions() {
      if (!this.isLeafNode) {
        // Intermediate nodes only temporarily cache VM regions for lazy tree
        // construction.
        return undefined;
      }
      return this.regions_;
    },

    get allRegionsForTesting() {
      if (this.regions_ !== undefined) {
        if (this.children_ !== undefined) {
          throw new Error('Internal error: a VM region classification node ' +
              'cannot have both regions and children');
        }
        // Leaf node (or caching internal node).
        return this.regions_;
      }

      // Intermediate node.
      let regions = [];
      this.children_.forEach(function(childNode) {
        regions = regions.concat(childNode.allRegionsForTesting);
      });
      return regions;
    },

    get isLeafNode() {
      const children = this.rule_.children;
      return children === undefined || children.length === 0;
    },

    addRegion(region) {
      this.addRegionRecursively_(region, true /* addStatsToThisNode */);
    },

    someRegion(fn, opt_this) {
      if (this.regions_ !== undefined) {
        // Leaf node (or caching internal node).
        return this.regions_.some(fn, opt_this);
      }

      // Intermediate node.
      return this.children_.some(function(childNode) {
        return childNode.someRegion(fn, opt_this);
      });
    },

    addRegionRecursively_(region, addStatsToThisNode) {
      if (addStatsToThisNode) {
        this.addStatsFromRegion_(region);
      }

      if (this.regions_ !== undefined) {
        if (this.children_ !== undefined) {
          throw new Error('Internal error: a VM region classification node ' +
              'cannot have both regions and children');
        }
        // Leaf node or an intermediate node caching VM regions (add the
        // region to this node and don't classify further).
        this.regions_.push(region);
        return;
      }

      // Non-leaf rule (classify region row further down the tree).
      function regionRowMatchesChildNide(child) {
        const fileRegExp = child.rule_.file;
        if (fileRegExp === undefined) return true;
        return fileRegExp.test(region.mappedFile);
      }

      let matchedChild = this.children_.find(regionRowMatchesChildNide);
      if (matchedChild === undefined) {
        // Region belongs to the 'Other' node (created lazily).
        if (this.children_.length !== this.rule_.children.length) {
          throw new Error('Internal error');
        }
        matchedChild = new VMRegionClassificationNode(
            VMRegionClassificationNode.OTHER_RULE);
        this.children_.push(matchedChild);
      }

      matchedChild.addRegionRecursively_(region, true);
    },

    buildTree_() {
      const cachedRegions = this.regions_;
      this.regions_ = undefined;

      this.buildChildNodesRecursively_();
      for (let i = 0; i < cachedRegions.length; i++) {
        // Note that we don't add the VM region's stats to this node because
        // they have already been added to it.
        this.addRegionRecursively_(
            cachedRegions[i], false /* addStatsToThisNode */);
      }
    },

    buildChildNodesRecursively_() {
      if (this.children_ !== undefined) {
        throw new Error(
            'Internal error: Classification node already has children');
      }
      if (this.regions_ !== undefined && this.regions_.length !== 0) {
        throw new Error(
            'Internal error: Classification node should have no regions');
      }

      if (this.isLeafNode) {
        return;  // Leaf node: Nothing to do.
      }

      // Intermediate node: Clear regions and build children recursively.
      this.regions_ = undefined;
      this.children_ = this.rule_.children.map(function(childRule) {
        const child = new VMRegionClassificationNode(childRule);
        child.buildChildNodesRecursively_();
        return child;
      });
    },

    addStatsFromRegion_(region) {
      this.hasRegions = true;

      // Aggregate virtual size.
      const regionSizeInBytes = region.sizeInBytes;
      if (regionSizeInBytes !== undefined) {
        this.sizeInBytes = (this.sizeInBytes || 0) + regionSizeInBytes;
      }

      // Aggregate byte stats.
      const thisByteStats = this.byteStats;
      const regionByteStats = region.byteStats;
      for (const byteStatName in regionByteStats) {
        const regionByteStatValue = regionByteStats[byteStatName];
        if (regionByteStatValue === undefined) continue;
        thisByteStats[byteStatName] =
            (thisByteStats[byteStatName] || 0) + regionByteStatValue;
      }

      // Aggregate java base.* stats.
      if (region.mappedFile.includes('/base.odex') ||
          region.mappedFile.includes('/base.vdex')) {
        if (region.byteStats.proportionalResident !== undefined) {
          thisByteStats.javaBasePss =
            (thisByteStats.javaBasePss || 0) +
            region.byteStats.proportionalResident;
        }
        if (region.byteStats.privateCleanResident !== undefined) {
          thisByteStats.javaBaseCleanResident =
            (thisByteStats.javaBaseCleanResident || 0) +
            region.byteStats.privateCleanResident;
        }
        if (region.byteStats.sharedCleanResident !== undefined) {
          thisByteStats.javaBaseCleanResident =
            (thisByteStats.javaBaseCleanResident || 0) +
            region.byteStats.sharedCleanResident;
        }
      }

      // Aggregate native library stats.
      const textProtectionFlags = (VMRegion.PROTECTION_FLAG_READ |
                                   VMRegion.PROTECTION_FLAG_EXECUTE);
      // On post-M devices, the native library is mapped from /base.apk. On M
      // and earlier devices, it's mapped from /libchrome.so. In both cases only
      // the regions that are readable and executable should be counted.
      // TODO(mattcary): if both mappings are seen, something has gone wrong and
      // some sort of error or fatal should be done. This should be tracked
      // across regions, which means adding state to |this|.
      if ((region.protectionFlags === textProtectionFlags) &&
          (region.mappedFile.includes('/base.apk') ||
           region.mappedFile.includes('/libchrome.so'))) {
        if (regionSizeInBytes !== undefined) {
          this.nativeLibrarySizeInBytes =
            (this.nativeLibrarySizeInBytes || 0) + regionSizeInBytes;
        }
        if (region.byteStats.privateCleanResident !== undefined) {
          thisByteStats.nativeLibraryPrivateCleanResident =
              (thisByteStats.nativeLibraryPrivateCleanResident || 0) +
              region.byteStats.privateCleanResident;
        }
        if (region.byteStats.sharedCleanResident !== undefined) {
          thisByteStats.nativeLibrarySharedCleanResident =
              (thisByteStats.nativeLibrarySharedCleanResident || 0) +
              region.byteStats.sharedCleanResident;
        }
        if (region.byteStats.proportionalResident !== undefined) {
          thisByteStats.nativeLibraryProportionalResident =
              (thisByteStats.nativeLibraryProportionalResident || 0) +
              region.byteStats.proportionalResident;
        }
      }
    }
  };

  return {
    VMRegion,
    VMRegionClassificationNode,
  };
});
</script>
